/*
 * Copyright 2011 Igor Maznitsa (http://www.igormaznitsa.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of version 3 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307  USA
 */
package com.igormaznitsa.jcp.expression;

import com.igormaznitsa.jcp.expression.functions.AbstractFunction;
import com.igormaznitsa.jcp.expression.operators.AbstractOperator;
import com.igormaznitsa.jcp.expression.operators.OperatorSUB;
import java.util.List;

/**
 * The class describes a wrapper around an expression item to be saved into an expression tree
 * @author Igor Maznitsa (igor.maznitsa@igormaznitsa.com)
 */
public class ExpressionTreeElement {

    /**
     * Inside constant to be used for speed up some operations
     */
    private static final OperatorSUB OPERATOR_SUB = AbstractOperator.findForClass(OperatorSUB.class);
    
    /**
     * Empty array to avoid unnecessary operations
     */
    private static final ExpressionTreeElement[] EMPTY = new ExpressionTreeElement[0];
    
    /**
     * The variable contains the wrapped expression item
     */
    private ExpressionItem savedItem;
    
    /**
     * The array contains links to the tree element children
     */
    private ExpressionTreeElement[] childElements;
    
    /**
     * The link to the parent element, if it is the tree root then it contains null
     */
    private ExpressionTreeElement parentTreeElement;
    
    /**
     * The priority of the tree element, it is very strongly used during tree sorting
     */
    private int priority;
    
    /**
     * Because I fill children sequentially, the variable contains the index of the first empty child slot
     */
    private int nextChildSlot = 0;

    /**
     * The constructor
     * @param item an expression item to be wrapped, must not be null
     */
    ExpressionTreeElement(final ExpressionItem item) {
        if (item == null){
            throw new NullPointerException("The item is null");
        }
        
        int arity = 0;
        if (item.getExpressionItemType() == ExpressionItemType.OPERATOR) {
            arity = ((AbstractOperator) item).getArity();
        } else if (item.getExpressionItemType() == ExpressionItemType.FUNCTION) {
            arity = ((AbstractFunction) item).getArity();
        }
        priority = item.getExpressionItemPriority().getPriority();
        this.savedItem = item;
        childElements = arity == 0 ? EMPTY : new ExpressionTreeElement[arity];
    }

    /**
     * Inside auxiliary function to set the maximum priority the the element 
     */
    void makeMaxPriority() {
        priority = ExpressionItemPriority.VALUE.getPriority();
    }

    /**
     * Get the wrapped item
     * @return the item to be wrapped by the object
     */
    public ExpressionItem getItem() {
        return this.savedItem;
    }

    /**
     * Get arity for the element (I mean possible children number)
     * @return the arity, zero for elements without children
     */
    public int getArity() {
        return childElements.length;
    }

    /**
     * Get the parent for the element
     * @return the parent for the element or null if the element is the tree root
     */
    public ExpressionTreeElement getParent() {
        return parentTreeElement;
    }

    /**
     * Get the current priority of the element
     * @return the priority
     */
    public int getPriority() {
        return priority;
    }

    /**
     * Add a tree as new child and make the maximum priority for it
     * @param tree a tree to be added as a child, must not be null
     * @return it returns this
     */
    public ExpressionTreeElement addSubTree(final ExpressionTree tree) {
        final ExpressionTreeElement root = tree.getRoot();
        if (root!=null){
            root.makeMaxPriority();
            addElementToNextFreeSlot(root);
        }
        return this;
    }

    /**
     * It replaces a child element
     * @param oldOne the old expression element to be replaced (must not be null)
     * @param newOne the new expression element to be used instead the old one (must not be null)
     * @return true if the element was found and replaced, else false
     */
    public boolean replaceElement(final ExpressionTreeElement oldOne, final ExpressionTreeElement newOne) {
        if (oldOne == null){
            throw new NullPointerException("The old element is null");
        }
        
        if  (newOne == null){
            throw new NullPointerException("The new element is null");
        }
        
        boolean result = false;
        
        final ExpressionTreeElement [] children = childElements;
        final int len = children.length;
        
        for (int i = 0; i < len; i++) {
            if (children[i] == oldOne) {
                children[i] = newOne;
                newOne.parentTreeElement = this;
                result = true;
                break;
            }
        }
        return result;
    }

    /**
     * Get the child element for its index (the first is 0)
     * @param index the index of the needed child
     * @return the child or null if the slot is empty
     * @throws ArrayIndexOutOfBoundsException it will be thrown if an impossible index is being used
     */
    public ExpressionTreeElement getChildForIndex(final int index) {
        return childElements[index];
    }

    /**
     * Add tree element with sorting operation depends on priority of the elements
     * @param element the element to be added, must not be null
     * @return the element which should be used as the last for the current tree
     */
    public ExpressionTreeElement addTreeElement(final ExpressionTreeElement element) {
        if (element == null) {
            throw new NullPointerException("The element is null");
        }
        
        final int newElementPriority = element.getPriority();

        ExpressionTreeElement result = this;

        final ExpressionTreeElement parentTreeElement = this.parentTreeElement;
        
        final int currentPriority = getPriority();

        if (newElementPriority < currentPriority) {
            if (parentTreeElement == null) {
                element.addTreeElement(this);
                result = element;
            } else {
                result = parentTreeElement.addTreeElement(element);
            }
        } else if (newElementPriority == currentPriority) {
            if (parentTreeElement != null) {
                parentTreeElement.replaceElement(this, element);
            }
            element.childElements[element.nextChildSlot] = this;
            element.nextChildSlot++;
            this.parentTreeElement = element;
            result = element;
        } else {
            if (isFull()) {
                final int lastElementIndex = getArity() - 1;

                final ExpressionTreeElement lastElement = childElements[lastElementIndex];
                if (lastElement.getPriority() > newElementPriority) {
                    element.addElementToNextFreeSlot(lastElement);
                    childElements[lastElementIndex] = element;
                    element.parentTreeElement = this;
                    result = element;
                }

            } else {
                addElementToNextFreeSlot(element);
                result = element;
            }
        }
        return result;
    }

    /**
     * It allows to check that all children slots have been filled
     * @return true if there is not any free child slot else false
     */
    public boolean isFull() {
        return nextChildSlot >= childElements.length;
    }

    /**
     * It fills children slots from a list containing expression trees
     * @param arguments the list containing trees to be used as children
     */
    public void fillArguments(final List<ExpressionTree> arguments) {
        if (arguments == null) {
            throw new NullPointerException("Argument list is null");
        }

        if (childElements.length != arguments.size()) {
            throw new IllegalArgumentException("Wrong argument list size");
        }

        int i = 0;
        for (ExpressionTree arg : arguments) {
            if (arg == null) {
                throw new NullPointerException("Argument [" + (i + 1) + "] is null");
            }

            if (childElements[i] != null) {
                throw new IllegalStateException("Non-null slot detected, it is possible that there is a program error, contact a developer please");
            }

            final ExpressionTreeElement root = arg.getRoot();
            if (root == null) {
                throw new IllegalArgumentException("Empty argument [" + (i + 1) + "] detected");
            }
            childElements[i] = root;
            root.parentTreeElement = this;

            i++;
        }
    }

    /**
     * Add an expression element into the next free child slot
     * @param element an element to be added, must not be null
     */
    private void addElementToNextFreeSlot(final ExpressionTreeElement element) {
        if (element == null){
            throw new NullPointerException("Element is null");
        }
        
        if (childElements.length == 0) {
            throw new IllegalArgumentException("The element doesn't support arguments [" + savedItem.toString() + ']');
        } else {
            if (isFull()) {
                throw new IllegalStateException("There is not any possibility to add new argument [" + savedItem.toString() + ']');
            } else {
                childElements[nextChildSlot++] = element;
            }
        }
        element.parentTreeElement = this;
    }

    /**
     * Post-processing after the tree is formed, the unary minus operation will be optimized
     */
    public void postProcess() {
        switch (savedItem.getExpressionItemType()) {
            case OPERATOR: {
                if (savedItem == OPERATOR_SUB) {
                    if (childElements[0] != null && childElements[1] == null) {
                        final ExpressionTreeElement left = childElements[0];
                        final ExpressionItem item = left.getItem();
                        if (item.getExpressionItemType() == ExpressionItemType.VALUE) {
                            final Value val = (Value) item;
                            if (val.getType() == ValueType.INT) {
                                childElements = EMPTY;
                                savedItem = Value.valueOf(Long.valueOf(0 - val.asLong().longValue()));
                                makeMaxPriority();
                            } else if (val.getType() == ValueType.FLOAT) {
                                childElements = EMPTY;
                                savedItem = Value.valueOf(Float.valueOf(0 - val.asFloat().floatValue()));
                                makeMaxPriority();
                            } else {
                                left.postProcess();
                            }
                        }
                    } else {
                        for (final ExpressionTreeElement element : childElements) {
                            if (element != null) {
                                element.postProcess();
                            }
                        }
                    }
                } else {
                    for (final ExpressionTreeElement element : childElements) {
                        if (element != null) {
                            element.postProcess();
                        }
                    }
                }
            }
            break;
            case FUNCTION: {
                for (final ExpressionTreeElement element : childElements) {
                    if (element != null) {
                        element.postProcess();
                    }
                }
            }
            break;
        }
    }
}
